Prévision des variations du bitcoin en fonction d'indicateurs techniques à partir de régression linéaire
Table des matières
- Contexte et objectifs du projet
- Travaux à réaliser
- Présentation des données
- Notations mathématiques
- Quelques ressources à consulter
- Démarche générale d'analyse
Figure 1 : Image générée par Midjourney
Contexte et objectifs du projet
Le bitcoin, une monnaie numérique décentralisée, fonctionne indépendamment de toute banque ou gouvernement. Il n'est pas émis par une autorité centrale, mais plutôt par des algorithmes exécutés sur un réseau informatique qui garantissent sa cohérence et la sécurité des transactions. Son potentiel économique est encore sous-évalué, mais le bitcoin a déjà fourni une alternative de financement pertinente à certains pays en développement par rapport aux monnaies traditionnelles contrôlées par des infrastructures bancaires ou des institutions étatiques. Cependant, la volatilité du bitcoin est parfois extrême, et comprendre ses fluctuations est un enjeu socio-économique majeur pour l'avenir.
Travaux à réaliser
Le but de ce projet est d'étudier l'influence d'indicateurs techniques financiers sur la valorisation du bitcoin (BTC). Cela sera fait en utilisant des outils d'analyse technique, qui sont généralement utilisés pour prédire les rendements futurs des actifs financiers en étudiant les données de marché historiques.
Tâches à accomplir
- Analyse descriptive univariée des variations de la cotation du BTC : Comprendre la distribution des rendements, évaluer la volatilité sur différentes périodes (hebdomadaire, mensuelle, annuelle, intra-journalière), etc.
- Analyse descriptive multivariée des variations du BTC : Explorer l'influence des indicateurs techniques financiers sélectionnés sur les variations de la cotation du BTC.
- Développement d'un ou plusieurs modèles explicatifs pour les variations du BTC : Créer des modèles qui peuvent expliquer les variations du BTC en fonction des indicateurs techniques, par exemple à partir de régressions linéaires.
- Évaluation des performances prédictives des modèles mis en œuvre : Évaluer la capacité des modèles développés à prédire les variations du BTC.
Outils et compétences nécessaires
Le travail est à réaliser avec le langage Python. Une montée en compétences est donc attendue en programmation Python, particulièrement sur la bibliothèque Pandas. En outre, il sera important de se familiariser avec le domaine de l'analyse financière pour comprendre comment les indicateurs techniques sont utilisés pour analyser et prédire les variations du marché.
Concernant Python la série de vidéos Formation Python Machine Learning est une ressource intéressante parmi d'autres pour se mettre à niveau avec ce langage.
Présentation des données
L'évolution de la cotation d'un actif au cours du temps est usuellement présentée sous la forme de données, dites au format OHLCV (de l'anglais open, high, low, close, volume). Il s'agit d'un format pratique permettant de représenter des séries temporelles d'une manière particulière. En effet, le format OHLC (la partie volume n'est pas concernée par la subtilité du format) représente l'évolution d'une mesure au cours d'intervalles de temps réguliers successifs caractérisés par une période d'échantillonnage notée \(\Delta t\) (e.g. une minute, cinq minutes, une heure, une journée, etc).
Typiquement, les données OHLCV contiennent les variables suivantes :
timestamp: timestamp caractérisant le début de la période de mesure (par exemple exprimé en millisecondes), la fin étant déterminée par la période d'échantillonnage \(\Delta t\) utilisée.open: valeur de l'actif au début de la période.high: valeur maximum de l'actif atteinte sur la période.low: valeur mimimum de l'actif atteinte sur la période.close: valeur de l'actif à la fin de la période.volume: montant cumulé des ventes et des achats de l'actif sur la période.
Il est courant de visualiser les données OHLC à l'aide du graphique, dit en chandeliers. En fonction, des valeurs d'ouverture (open) et de fermeture (close), on parle également de chandeliers haussiers et baissiers, cf. Figure 2.
Figure 2 : Représentation du symbole de chandelier.
Dans un diagramme en chandelier, on retrouve les données OHLC mais également les notions de :
- corps (body) représentant la distance entre la cotation d'ouverture et de fermeture de l'actif sur une période ;
- mèches ou ombres supérieures (upper shadow) représentant la distance entre la partie haute du corps et la cotation la plus haute sur une période ;
- mèches ou ombres inférieures (lower shadow) représentant la distance entre la partie basse du corps et la cotation la plus basse sur une période ;
Dans ce projet nous allons travailler sur les données de cotation du Bitcoin (BTC) par rapport au Dollar (USDT) échantillonnées à une période de 5 minutes sur les années 2021 et 2022.
Le fichier de données est accessible en suivant ce lien de téléchargement. Le tableau suivant donne un aperçu des 25 premières minutes de cotations du BTC/USDT de l'année 2021.
import pandas as pd ohlcv_df = pd.read_csv("btc_5m_2021_2022.csv.bz2", compression="bz2", index_col="time") ohlcv_df
| open | high | low | close | volume | |
|---|---|---|---|---|---|
| time | |||||
| 2021-01-01 00:00:00+00:00 | 28 923.63 | 29 017.50 | 28 913.12 | 28 975.65 | 182.89 |
| 2021-01-01 00:05:00+00:00 | 28 975.65 | 28 979.53 | 28 846.28 | 28 858.94 | 214.57 |
| 2021-01-01 00:10:00+00:00 | 28 858.94 | 28 883.20 | 28 690.17 | 28 752.80 | 442.62 |
| 2021-01-01 00:15:00+00:00 | 28 752.80 | 28 852.48 | 28 720.91 | 28 820.72 | 174.84 |
| 2021-01-01 00:20:00+00:00 | 28 822.17 | 28 846.46 | 28 744.09 | 28 846.46 | 161.32 |
Par exemple, la première ligne du tableau signifie que le 01/01/2021 :
- à 00:00:00, le prix d'un BTC était de 28923.63 USDT ;
- à 00:05:00, le prix d'un BTC était de 28975.65 USDT (cotation de fermeture de la période de 00:00:00 et cotation d'ouverture de la période 00:05:00) ;
- entre 00:00:00 et 00:05:00 le prix d'un BTC est passé par un minimum de 28913.12 USDT et un maximum de 29017.5 USDT.
Note : Le marché BTC/USDT étant ouvert 24h/24 et 7j/7, nous devrions observer \(\text{close}(t-1)\) égale à \(\text{open}(t)\) pour tout \(t\). Toutefois, la collecte des données n'étant pas instantanée, il est fréquent d'observer de légères différences entre le cours d'ouverture de la période courante et le cours de fermeture de la période précédente.
Notations mathématiques
Notions temporelles
Nous avons introduit précédemment la période d'échantillonnage des données \(\Delta t\). On note également \(t_{0}\) la première date d'échantillonnage. Dans l'exemple de données précédent, on rappelle que \(\Delta t = 5\) minutes et que \(t_{0} =\) 2021-01-01 00:00:00.
Nous introduisons ensuite l'indice \(p \ge 0\) permettant de référencer la \(p\) -ème période d'observation des données (cf. Figure 3), i.e. entre \(t_{0} + p\Delta t\) et \(t_{0} + (p+1)\Delta t\). En reprenant l'exemple précédent, on a :
- 1er période d'observation \(p = 0\) : période entre 2021-01-01 00:00:00 et 2021-01-01 00:05:00 ;
- 5ème période d'observation \(p = 4\) : période entre 2021-01-01 00:20:00 et 2021-01-01 00:25:00.
Figure 3 : Notion de période sur les données OHLCV.
Les données OHLCV sont notées dans la suite comme suit :
- \(\text{o}_{p}\) : cotation de l'actif à l'ouverture de la période \(p\), i.e. à \(t = t_{0} + p\Delta t\) ;
- \(\text{c}_{p}\) : cotation de l'actif à la fermeture de la période \(p\), i.e. à \(t = t_{0} + (p+1)\Delta t\) ;
- \(\text{h}_{p}\) : cotation maximum de l'actif sur la période \(p\) ;
- \(\text{l}_{p}\) : cotation mimimum de l'actif sur la période \(p\) ;
- \(\text{v}_{p}\) : volume échangé de l'actif la période \(p\).
Rendements
On note enfin \(r_{p}\) la variation relative de la cotation d'un actif entre deux périodes consécutives, on parle également de rendement (ou returns en anglais) sur la période \(p\). Nous avons donc pour tout \(p \ge 1\) :
\begin{equation} \label{org0db3dc9} r_{p} = \frac{\text{c}_{p}}{\text{c}_{p - 1}} - 1. \end{equation}La quantité \(r_{p}\) est donc positive si le BTC gagne en valeur par rapport à l'USDT sur la période \(p\) et négative sinon. Le rendement représente l'indicateur principal au coeur de toutes les analyses financières réalisées sur un actif donné.
La définition \eqref{org0db3dc9} porte sur un rendement entre deux périodes successives. Cette définition peut être étendue entre deux périodes \(p\) et \(p + k\) données (cf. Figure 4) comme suit :
\begin{equation} \rho_{p + k} = \frac{\text{c}_{p + k}}{\text{c}_{p-1}} - 1, \end{equation}avec \(k \ge 0\) un nombre entier permettant de définir le nombre de périodes futures à prendre en compte pour le calcul du rendement.
Figure 4 : Illustration graphique du calcul d'un rendement de clôture.
Exemples
Nous illustrons les définitions précédentes sur l'extrait de cotation BTC/USDT suivant.
import plotly.graph_objects as go ohlcv_ex_df = ohlcv_df.head(10) fig = go.Figure() fig.add_trace(go.Candlestick(x=ohlcv_ex_df.index, open=ohlcv_ex_df["open"], high=ohlcv_ex_df["high"], low=ohlcv_ex_df["low"], close=ohlcv_ex_df["close"], name="OHLC")) fig.update_layout(title="Exemple de données OHLC sur l'actif BTC/USDT", yaxis_title="USDT", xaxis_title="Temps", xaxis_rangeslider_visible=False) fig
Le calcul général du rendement de clôture à \(p + k\) avec Pandas repose sur la méthode .shift :
rho_pk = ohlcv_df["close"].shift(-k)/ohlcv_df["close"].shift(1) - 1
Un calcul alternatif équivalent utilise la méthode .pct_change :
rho_pk = ohlcv_df["close"].pct_change(k+1).shift(-k)
Même si à notre niveau les deux méthodes précédentes sont identiques, on privilégiera dans la suite
l'utilisation de la méthode .pct_change car cette dernière possède une meilleure précision
numérique.
Calculons les rendements à \(p\) et \(p + 2\) :
#rho_p = (ohlcv_ex_df["close"].shift(0)/ohlcv_ex_df["close"].shift(1) - 1).rename("returns") #rho_p2 = (ohlcv_ex_df["close"].shift(-2)/ohlcv_ex_df["close"].shift(1) - 1).rename("returns_2") rho_p = ohlcv_ex_df["close"].pct_change(1).shift(0).rename("returns") # => équivalent à écrire rho_p = ohlcv_ex_df["close"].pct_change().rename("returns") rho_p2 = ohlcv_ex_df["close"].pct_change(3).shift(-2).rename("returns_2") rho_p_p2 = pd.concat([rho_p, rho_p2], axis=1) rho_p_p2
| returns | returns_2 | |
|---|---|---|
| time | ||
| 2021-01-01 00:00:00+00:00 | ||
| 2021-01-01 00:05:00+00:00 | -0.403% | -0.535% |
| 2021-01-01 00:10:00+00:00 | -0.368% | -0.043% |
| 2021-01-01 00:15:00+00:00 | 0.236% | 0.292% |
| 2021-01-01 00:20:00+00:00 | 0.089% | 0.207% |
| 2021-01-01 00:25:00+00:00 | -0.034% | 0.179% |
| 2021-01-01 00:30:00+00:00 | 0.152% | 0.324% |
| 2021-01-01 00:35:00+00:00 | 0.061% | 0.228% |
| 2021-01-01 00:40:00+00:00 | 0.111% | |
| 2021-01-01 00:45:00+00:00 | 0.056% |
En prenant la période \(p\) = 2021-01-01T00:05:00 et un horizon \(k = 0\), le rendement de clôture \(\rho_{p} = r_{p}\) correspond au rapport entre la clôture de la période 2021-01-01T00:05:00 et la clôture de la période 2021-01-01T00:00:00, soit : \[ \rho_{p} = \frac{\text{c}_{p}}{\text{c}_{p-1}} = \frac{28858.94}{28975.65} - 1 \simeq -0.004028 \simeq -0.403 \% \]
En prenant la période \(p\) = 2021-01-01T00:10:00 et un horizon \(k = 2\), le rendement de clôture \(\rho_{p+2}\) correspond au rapport entre la clôture de la période 2021-01-01T00:20:00 et la clôture de la période 2021-01-01T00:05:00, soit : \[ \rho_{p + 2} = \frac{\text{c}_{p+2}}{\text{c}_{p-1}} = \frac{28846.46}{28858.94} - 1 \simeq -0.000432 \simeq -0.043 \% \]
Propriété
Le rendement de clôture entre la période \(p\) et la période \(p + k\), noté \(\rho_{p + k}\), s'exprime en fonction des rendements \(r_{p}, \ldots, r_{p+k}\) (cf. Équation \eqref{org0db3dc9}) comme suit :
\begin{equation} \rho_{p + k} = \prod_{i = 1}^{k} (r_{p + i} + 1) - 1 \end{equation}Ce résultat classique s'obtient en remarquant que :
\begin{align} \rho_{p + k} + 1 & = \frac{\text{c}_{p + k}}{\text{c}_{p-1}} \\ & = \frac{\text{c}_{p}}{\text{c}_{p - 1}} \frac{\text{c}_{p + 1}}{\text{c}_{p}} \ldots \frac{\text{c}_{p + k}}{\text{c}_{p + k - 1}} \\ & = (r_{p} + 1) (r_{p+1} + 1) \ldots (r_{p+k} + 1) \end{align}Quelques ressources à consulter
L'objectif du projet consiste à expliquer les rendements futurs de la cotation BTC/USDT. En effet, prévoir avec un bon niveau de précision les prochaines variations de la cotation du BTC/USDT, permettrait d'élaborer des stratégies d'investissement rentables. Dans le domaine de la finance des marchés, ce travail est réalisé par les analystes quantitatifs (également appelés quant).
Voici quelques ressources à consulter afin de montée en compétences dans le domaine de l'analyse financière et du bitcoin :
- Le Bitcoin et la Blockchain
- La formule qui a radicalement transformé la finance mondiale [Black-Scholes
- Krachs Boursiers & Tremblements De Terre
- Les bases de L'analyse Technique
- Trading RSI sur le Bitcoin
À la lumière ces nouvelles connaissances, il peut être intéressant de relire la Section Notations mathématiques.
Question à méditer : En supposons que l'on dispose d'un modèle permettant, à chaque période \(p\), de prévoir le rendement du BTC/USDT à \(k\) unités de temps dans le futur, i.e. \(\rho_{p+k}\). Comment pourriez-vous utiliser cette information pour investir dans ce marché ?
Démarche générale d'analyse
Les paragraphes suivants présente une démarche d'analyse statistique générale d'un actif financier appliquée dans le cas de l'analyse de la cotation BTC/USDT.
Construction de la variable cible
Dans notre problématique, la variable cible (c.-à-d. la variable à prédire) représente les rendements futurs du BTC/USDT. Dans un premier temps, il est donc nécessaire de décider de l'horizon futur des rendements à prévoir. Prenons un exemple en considérant un horizon d'une heure. Sachant que les données utilisées sont échantillonnées avec une période de 5 min, il faut calculer à chaque période \(p\) le rendement à \(p + k\) où \(k = 60~\text{min} / 5~\text{min} = 12\).
Commençons par charger les données.
import pandas as pd ohlcv_df = pd.read_csv("btc_5m_2021_2022.csv.bz2", compression="bz2", index_col="time")
Calculons ensuite les rendements futurs sur un horizon de 1 heure (soit \(k=12\) unités de temps).
k_horizon = 12 returns = ohlcv_df["close"].pct_change(k_horizon+1).shift(-k_horizon).rename(f"r{k_horizon}")
Analyse descriptive univariée
Nous pouvons alors faire une analyse univariée rapide de notre variable cible.
Statistiques de base :
returns.describe()
| r12 | |
|---|---|
| count | 2.1023e+05 |
| mean | 2.2043e-06 |
| std | 0.008571 |
| min | -0.22891 |
| 25% | -0.0034036 |
| 50% | 3.6285e-05 |
| 75% | 0.003419 |
| max | 0.17927 |
Premières valeurs (1 semaine = 2016*5 min)
import plotly.express as px fig_returns = px.line(returns.head(2016), title=f"Rendements du BTC/USDT à p+{k_horizon}", markers=True, labels=dict( value=returns.name)) fig_returns.update_layout(showlegend=False) fig_returns
Distribution
import plotly.express as px fig_returns_hist = px.histogram(returns, title="Distribution des rendements du BTC/USDT à p+12", labels=dict(value=returns.name)) fig_returns_hist.update_layout(showlegend=False) fig_returns_hist
Question à méditer : En tant qu'analyste quantitif, que vous apprennent ces premières analyses sur les rendements du BTC/USDT sur un horizon futur de 1h ?
Création des variables explicatives
Dans ce projet, nous proposons d'expliquer les variations du BTC/USDT à partir d'indicateurs techniques financiers. Il existe un très grand nombre d'indicateurs techniques utilisés classiquement dans le domaine de la finance. Parmi les plus connus, on trouve :
- le RSI (Relative Strength Index) ;
- le MACD (Moving Average Convergence Divergence) ;
- les Bandes de Bollinger ;
- le Stochastique.
Tous ces indicateurs se calculent à partir des données OHLCV du BTC/USDT disponibles. De plus, la
librairie pandas_ta (pip install pandas_ta) met à disposition les fonctions de calcul des principaux indicateurs
techniques.
Calcul de l'indicateur RSI
Le RSI est un indicateur technique permettant d'évaluer la vitesse et le changement des mouvements de prix d'un actif. Le RSI oscille entre 0 et 100 et est principalement utilisé pour identifier les conditions de surachat et de survente. Si l'indicateur RSI est grand (e.g. supérieur à 70), cela suggère que l'actif est suracheté, ce qui signifie que le prix pourrait baisser à l'avenir. En revanche, si l'indicateur est faible (e.g. inférieur à 30), cela peut indiquer que l'actif est survendu et que le prix pourrait augmenter.
L'indicateur RSI possède un paramètre appelé période ou fenêtre du RSI. Ce paramètre détermine le nombre de périodes (dans notre cas une période = 5 min) à prendre en compte pour le calcul de l'indicateur. La période du RSI influence la sensibilité de l'indicateur. Une période plus courte rendra le RSI plus sensible aux variations de prix, produisant plus de signaux de surachat et de survente. En revanche, une période plus longue rendra le RSI moins sensible, produisant moins de ces signaux, et dans ce cas, ces derniers peuvent être considérés comme plus fiables.
Notes :
- comme tous les indicateurs techniques, le RSI n'est pas parfait et ne permet pas à lui seul de prédire le cours futur d'un actif. En revanche, il peut être intéressant de mesurer son pouvoir explicatif, puis de l'utiliser en conjonction avec d'autres indicateurs techniques.
- Il n'y a pas de "meilleure" période de RSI universelle ; le choix dépend de la stratégie d'investissement, de la volatilité de l'actif, et d'autres facteurs. Il est toujours recommandé de tester différentes périodes pour voir laquelle fonctionne le mieux pour la stratégie d'investissement considérée.
Calculons par exemple le RSI avec une fenêtre de 25 minutes (i.e. 5 périodes de 5 min).
import pandas_ta as ta # Calcul du RSI avec une fenêtre d'un 25min (5 périodes de 5 minutes) rsi = ta.rsi(ohlcv_df['close'], length=5) rsi
Notes :
Pour calculer le RSI, vous devez avoir au moins autant de points de données que la taille
de la fenêtre spécifiée. Ceci explique pourquoi les 5 premières valeurs de la Series rsi sont
à NaN.
Visualisation du RSI(5) sur 1 semaine = 2016*5 min.
fig_rsi = px.line(rsi.head(2016), title=f"{rsi.name}", markers=True, labels=dict( value=rsi.name)) fig_rsi.update_layout(showlegend=False) fig_rsi.update_yaxes(range=[0, 100]) fig_rsi
À ce stade, nous pouvons créer un DataFrame qui contiendra tous les indicateurs que nous souhaitons utiliser
pour prédire les rendements du BTC/USDT :
indics_df = pd.DataFrame(index=ohlcv_df.index)
Ajoutons des indicateurs RSI en changeant la fenêtre de calcul :
indics_df.ta.rsi(close=ohlcv_df["close"], length=5, append=True) indics_df.ta.rsi(close=ohlcv_df["close"], length=10, append=True) indics_df.ta.rsi(close=ohlcv_df["close"], length=30, append=True) indics_df
Calcul de l'indicateur MFI
Le Money Flow Index (MFI) est un indicateur variant entre 0 et 100 mesurant la pression d'achat et de vente en tenant compte à la fois des prix et des volumes. Un MFI élevé (proche de 100) est généralement considéré comme synonyme d'un actif suracheté, tandis qu'un MFI faible (proche de 0) est généralement considéré synonyme d'un actif survendu.
Calculons l'MFI sur une fenêtre de 10 unités de temps.
import pandas_ta as ta mfi = ta.mfi(close=ohlcv_df['close'], high=ohlcv_df['high'], low=ohlcv_df['low'], volume=ohlcv_df['volume'], length=10)
Notes : Le calcul de l'MFI nécessite les prix de clôture, du minimum, du maximum et les volumes échangés à chaque période.
Visualisation du MFI sur 1 semaine = 2016*5 min.
fig_mfi = px.line(mfi.head(2016), title=f"{mfi.name}", markers=True, labels=dict( value=mfi.name)) fig_mfi.update_layout(showlegend=False) fig_mfi.update_yaxes(range=[0, 100]) fig_mfi
Calcul de différents indicateurs MFI en changeant la fenêtre de calcul :
length_list = [5, 10, 30] for length in length_list: indics_df.ta.mfi(close=ohlcv_df['close'], high=ohlcv_df['high'], low=ohlcv_df['low'], volume=ohlcv_df['volume'], length=length, append=True)
Analyse exploratoire multivariée
Dans ce projet, le but de l'analyse exploratoire multivariée consiste à identifier d'éventuelles liens entre les variables explicatives retenues et la variable cible (rendements à 1h).
Pour ce faire, nous pouvons réaliser une analyse visuelle avec un diagramme en paires.
data_pairs_df = pd.concat([indics_df, returns], axis=1) fig_pairs = px.scatter_matrix(data_pairs_df.head(10000), title="Diagramme en paire") fig_pairs
Nous pouvons également visualiser uniquement les nuages de points de la variable cible avec les différentes
variables explicatives en y ajoutant une droite de régression linéaires (option trendline de la
fonction px.scatter).
data_target_features_long_df = \ pd.melt(data_pairs_df.reset_index().head(10000), id_vars=["time", returns.name], value_vars=list(indics_df.columns), var_name="indic", value_name="indic_value", ) fig_target_pairs = \ px.scatter( data_target_features_long_df, x="indic_value", y=returns.name, color="indic", facet_row="indic", trendline="ols", title=f"Variable cible {returns.name} en fonction d'indicateurs techniques", ) fig_target_pairs
Questions à méditer :
- Quelles sont les informations à retenir de cette première analyse visuelle ?
- Par quelle(s) méthode(s) poursuivre l'analyse multivariée ?
Modèle 1 : Premier modèle de régression linéaire univarié
Les variables explicatives et la variable cible étant quantitatives, la régression linéaire semble a priori une méthode adéquate pour répondre à notre problématique.
Nous choisissons dans un premier temps d'expliquer les rendements à \(p + 12\) (soit les rendements a 1 heure dans le futur) à partir de l'indicateur RSI avec une période de taille 5 en utilisant une régression linéaire. On pose donc l'hypothèse suivante : \[ \text{r12} = \alpha_{0} + \alpha_{1} \text{RSI5}. \] où $α0 et α1 sont les paramètres de régression à estimer à partir des données.
Pour ce faire, nous utilisons la librairie statsmodels (pip install statsmodels).
import statsmodels.api as sm
On déclare notre variable cible et on construit le jeu de données contenant à la fois les variables explicatives et la variable cible.
var_target = f"r{k_horizon}" # On applique un décalage d'une unité de temps aux variables explicatives # car au début d'une période, on ne peut pas prédire un rendement futur en utilisant # des indicateurs utilisant la valeur de clôture de cette même période. data_all = pd.concat([indics_df.shift(1), returns], axis=1).dropna() target = data_all[var_target]
La régression linéaire est mise en oeuvre grâce à la classe OLS comme suit :
var_features_mod1 = ["RSI_5"] # Ajout d'une colonne de 1 afin d'ajouter le paramètre $\alpha_{0}$ features_mod1_df = sm.add_constant(data_all[var_features_mod1]) mod1 = sm.OLS(target, features_mod1_df) mod1_res = mod1.fit() mod1_res.summary()
| Coefficient | Écart-type | p-values | IC 2.5% | IC 97.5% | |
|---|---|---|---|---|---|
| const | 0.00022219 | 5.4502e-05 | 4.57e-05 | 0.00011536 | 0.00032901 |
| RSI_5 | -4.4455e-06 | 1.0226e-06 | 1.3802e-05 | -6.4498e-06 | -2.4411e-06 |
Questions à méditer :
- Avez-vous bien compris pourquoi il est nécessaire de faire
indics_df.shift(1)pour décaler les variables explicatives d'une unité de temps avant de construire le modèle de prévision des rendements ? - À quoi sert la commande
sm.add_constant? - Comment interpréter les coefficients obtenus ?
Modèle 2 : Régression linéaire multivariée
Nous choisissons à présent d'expliquer les rendements à \(p + 12\) à partir d'indicateurs RSI et MFI en utilisant toujours une régression linéaire mais cette fois-ci multivariée : \[ \text{r12} = \alpha_{0} + \alpha_{1} \text{RSI5} + \alpha_{2} \text{RSI10} + \alpha_{3}\text{RSI30}+ \alpha_{4} \text{MFI5} + \alpha_{5} \text{MFI10} + \alpha_{6}\text{MFI30} \] où \(\alpha_{0}, \alpha_{1}, \ldots, \alpha_{6}\) sont les paramètres de régression à estimer à partir des données.
Comme précédemment, on met en oeuvre la régression linéaire comme suit :
var_features_mod2 = list(indics_df.columns) # Ajout d'une colonne de 1 afin d'ajouter le paramètre $\alpha_{0}$ features_mod2_df = sm.add_constant(data_all[var_features_mod2]) mod2 = sm.OLS(target, features_mod2_df) mod2_res = mod2.fit() mod2_res.summary()
| Coefficient | Écart-type | p-values | IC 2.5% | IC 97.5% | |
|---|---|---|---|---|---|
| const | 0.00018953 | 0.00014626 | 0.19503 | -9.7138e-05 | 0.00047621 |
| RSI_5 | -1.2246e-05 | 4.2415e-06 | 0.003887 | -2.0559e-05 | -3.9329e-06 |
| RSI_10 | 1.3738e-05 | 7.8782e-06 | 0.081186 | -1.7026e-06 | 2.918e-05 |
| RSI_30 | -1.908e-06 | 6.6264e-06 | 0.7734 | -1.4896e-05 | 1.108e-05 |
| MFI_5 | -7.4741e-07 | 1.154e-06 | 0.51721 | -3.0092e-06 | 1.5144e-06 |
| MFI_10 | 2.033e-06 | 9.4243e-07 | 0.030992 | 1.8585e-07 | 3.8801e-06 |
| MFI_30 | -4.7051e-06 | 2.6371e-06 | 0.07439 | -9.8737e-06 | 4.6349e-07 |
Évaluation des modèles
Questions :
- Quelle approche standard d'évaluation d'un modèle prédictif connaissez-vous ?
- Cette approche est-elle adaptée dans le contexte de ce projet ?
- Du point de vue d'un analyste quantitatif, quels seraient les critères permettant de dire qu'un modèle est bon ou mauvais ?
Pour évaluer les modèles, nous mettons en oeuvre la démarche d'apprentissage/test usuelle. Les données étant chronologiques, nous veillons à ce que les données d'apprentissage correspondent bien à des données antérieures aux données de test.
pct_train = 0.7 nb_train = int(pct_train*len(data_all)) data_all_train = data_all.iloc[:nb_train] data_all_test = data_all.iloc[nb_train:] target_train = data_all_train[var_target] target_test = data_all_test[var_target] ohlcv_train_df = ohlcv_df.loc[data_all_train.index] ohlcv_test_df = ohlcv_df.loc[data_all_test.index]
Évaluation du modèle 1
Extraction des variables explicatives pour le modèle 1.
features_mod1_train_df = sm.add_constant(data_all_train[var_features_mod1]) features_mod1_test_df = sm.add_constant(data_all_test[var_features_mod1])
Un modèle de régression linéaire est ensuite ajusté sur les données d'apprentissage.
mod1_train = sm.OLS(target_train, features_mod1_train_df) mod1_train_res = mod1_train.fit() mod1_train_res.summary()
| Coefficient | Écart-type | p-values | IC 2.5% | IC 97.5% | |
|---|---|---|---|---|---|
| const | 0.00034364 | 6.9892e-05 | 8.8102e-07 | 0.00020665 | 0.00048062 |
| RSI_5 | -6.0812e-06 | 1.3102e-06 | 3.4614e-06 | -8.6491e-06 | -3.5133e-06 |
Puis, ce modèle est utilisé pour estimer des rendements à \(p + 12\).
mod1_target_pred = mod1_train_res.predict(features_mod1_test_df)
Il est alors possible d'évaluer les performances du modèle en utilisant un indicateur classique comme l'erreur absolue moyenne.
mod1_pred_mae = (mod1_target_pred - target_test).abs().mean()
Toutefois, dans une application d'analyse financière, l'enjeu est d'élaborer un modèle d'investissement rentable. Par exemple, on peut chercher à évaluer la rentabilité de la stratégie suivante : À une période \(p\) donnée, si le modèle de régression linéaire prédit un rendement à \(p+12\) positif, alors un achat est effectué et on ne fait rien sinon. Si un achat est réalisé à la période \(p\), une vente a lieu à \(p + 12\) obligatoirement.
La mise en oeuvre de cette stratégie se déroule comme suit :
# Seuil d'achat buy_thresh = 0.00 # Signaux d'achat idx_mod1_pred_buy = mod1_target_pred > buy_thresh # Signaux de vente idx_mod1_pred_sell = idx_mod1_pred_buy.shift(k_horizon - 1).fillna(False)
Maintenant que nous disposons des instants d'achat et de vente de notre stratégie, nous pouvons la backtester, c.-à-d. évaluer sa performance financière. Pour ce faire, nous allons simuler l'impact financier des achats et des ventes de notre stratégie en utilisant les données historiques à notre disposition.
Du point de vue du vocabulaire, on appelera quote la monnaie qui sert à la cotation du Bitcoin. Ici les Bitcoins sont valorisés en USDT, donc la monnaie quote est l'USDT. On appelera base la monnaie sur laquelle on investit. Ici, il s'agit du Bitcoin, donc la monnaie base est le BTC.
Nous supposons dans la suite que nous partons d'un fond d'investissement de 1 USDT et que nous investissons à chaque achat, la somme de 1/12 USDT.
quote_init = 1 quote_trade_invest = 1/k_horizon
Enfin, nous utilisons la fonction de backtesting suivante permettant de calculer la performance de notre stratégie.
def backtest_fix_sell( ohlcv_df, idx_buy, idx_sell, quote_init=1, quote_trade_invest=1, base_name="base", quote_name="quote", buy_on="open", sell_on="close", balance_on="close", ): trades_quote_ob = \ pd.Series(0, index=ohlcv_df.index, name=f"{quote_name}:ob") trades_base_ob = \ pd.Series(0, index=ohlcv_df.index, name=f"{base_name}:ob") trades_quote_ob[idx_buy] += -quote_trade_invest trades_base_ob[idx_buy] = \ quote_trade_invest/ohlcv_df[buy_on] trades_base_os = \ (-trades_base_ob.shift(k_horizon - 1)\ .rename(f"{base_name}:os"))\ .fillna(0) trades_quote_os = \ ((-trades_base_os)*\ ohlcv_df[sell_on])\ .rename(f"{quote_name}:os").fillna(0) trades_quote = (trades_quote_ob + trades_quote_os).rename(f"{quote_name}:o") trades_base = (trades_base_ob + trades_base_os).rename(f"{base_name}:o") trades_base_quote = \ (trades_base*ohlcv_df.loc[trades_base.index, balance_on])\ .rename(f"{base_name}-{quote_name}") trades_quote_balance = trades_quote + trades_base_quote trades_quote_balance.iloc[0] += quote_init trades_quote_balance = trades_quote_balance.cumsum().rename(f"{quote_name}:balance") trades_quote_perf = (trades_quote_balance/quote_init).rename(f"{quote_name}:perf") trades_df = pd.concat([ trades_quote_ob, trades_base_ob, trades_quote_os, trades_base_os, trades_quote, trades_base, trades_base_quote, trades_quote_balance, trades_quote_perf, ], axis=1) return trades_df
mod1_trades_df = \ backtest_fix_sell( ohlcv_test_df, idx_buy=idx_mod1_pred_buy, idx_sell=idx_mod1_pred_sell, quote_init=quote_init, quote_trade_invest=quote_trade_invest, base_name="BTC", quote_name="USDT", )
Visualisation de la performance de la stratégie sur la période de test.
fig_mod1_perf = px.line(mod1_trades_df["USDT:perf"], title=f"Modèle 1 : Performance de la stratégie", markers=False, labels=dict( value="Performance")) fig_mod1_perf.update_layout(showlegend=False) fig_mod1_perf.update_yaxes(range=[0, mod2_trades_df["USDT:perf"].max()*1.05]) fig_mod1_perf.update_traces(line=dict(width=5)) fig_mod1_perf
Évaluation du modèle 2
La démarche d'évaluation du modèle 2 est analogue.
Extraction des variables explicatives pour le modèle 1.
features_mod2_train_df = sm.add_constant(data_all_train[var_features_mod2]) features_mod2_test_df = sm.add_constant(data_all_test[var_features_mod2])
Création du modèle de régression linéaire.
mod2_train = sm.OLS(target_train, features_mod2_train_df) mod2_train_res = mod2_train.fit() mod2_train_res.summary()
| Coefficient | Écart-type | p-values | IC 2.5% | IC 97.5% | |
|---|---|---|---|---|---|
| const | 0.00028873 | 0.00019127 | 0.13116 | -8.615e-05 | 0.00066361 |
| RSI_5 | -1.8718e-05 | 5.4322e-06 | 0.00056949 | -2.9366e-05 | -8.0714e-06 |
| RSI_10 | 2.5279e-05 | 1.0052e-05 | 0.011908 | 5.5777e-06 | 4.498e-05 |
| RSI_30 | -5.0204e-06 | 8.6239e-06 | 0.56047 | -2.1923e-05 | 1.1882e-05 |
| MFI_5 | -1.9712e-06 | 1.4592e-06 | 0.17674 | -4.8312e-06 | 8.8882e-07 |
| MFI_10 | 2.1421e-06 | 1.0609e-06 | 0.043477 | 6.2739e-08 | 4.2215e-06 |
| MFI_30 | -6.811e-06 | 3.3534e-06 | 0.042248 | -1.3384e-05 | -2.3845e-07 |
Prédiction des rendements à \(p + 12\).
mod2_target_pred = mod2_train_res.predict(features_mod2_test_df)
Calcul de l'erreur absolue moyenne.
mod2_pred_mae = (mod2_target_pred - target_test).abs().mean()
Toutefois, dans une application d'analyse financière, l'enjeu est d'élaborer un modèle d'investissement rentable. Par exemple, on peut chercher à évaluer la rentabilité de la stratégie suivante : À une période \(p\) donnée, si le modèle de régression linéaire prédit un rendement à \(p+12\) positif, alors un achat est effectué et on ne fait rien sinon. Si un achat est réalisé à la période \(p\), une vente a lieu à \(p + 12\) et le rendement à \(p+12\) est obtenu (positif ou négatif).
Évaluation de la stratégie d'investissement décrite dans la section précédente. Visualisation de la performance de la stratégie sur la période de test.
#mod2_perf = mod2_trades_trades["USDT:balance"].cumsum().rename("perf") fig_mod2_perf = \ px.line(mod2_trades_df["USDT:perf"], title=f"Modèle 2 : Performance de la stratégie", labels=dict( value="Performance")) fig_mod2_perf.update_layout(showlegend=False) fig_mod2_perf.update_yaxes(range=[0, mod2_trades_df["USDT:perf"].max()*1.05]) fig_mod2_perf.update_traces(line=dict(width=5)) fig_mod2_perf
Comparaison des performances modèles
Nous pouvons enfin visualiser sur un même graphique les performances de nos deux modèles ainsi que la performance naturelle du BTC/USDT sur la même période.
La performance naturelle du BTC/USDT correspond simplement à la performance d'un unique achat pour un USDT au début de la période de l'étude et de la vente de notre actif (en BTC) à la fin de la période d'étude :
ohlcv_test_perf = (ohlcv_test_df["close"]/ohlcv_test_df["close"].iloc[0]).rename("USDT:perf")
Visualisation des performances.
models_perf_df = pd.concat([ ohlcv_test_perf.reset_index().assign(model="btc/usdt"), mod1_trades_df["USDT:perf"].reset_index().assign(model="modèle 1"), mod2_trades_df["USDT:perf"].reset_index().assign(model="modèle 2"), ], axis=0) fig_models_perf = px.line(models_perf_df, x="time", y="USDT:perf", color="model", markers=False, title="Comparaison des performances des stratégies", labels=dict( perf="Performance")) fig_models_perf.update_yaxes( range=[0, models_perf_df["USDT:perf"].max()*1.05]) fig_models_perf.update_traces(line=dict(width=5)) fig_models_perf